3 namespace MediaWiki\Tests\Revision
;
7 use InvalidArgumentException
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Revision\RevisionAccessException
;
11 use MediaWiki\Revision\RevisionStore
;
12 use MediaWiki\Revision\SlotRoleRegistry
;
13 use MediaWiki\Revision\SlotRecord
;
14 use MediaWiki\Storage\SqlBlobStore
;
15 use MediaWikiTestCase
;
19 use Wikimedia\Rdbms\Database
;
20 use Wikimedia\Rdbms\LoadBalancer
;
21 use Wikimedia\TestingAccessWrapper
;
27 class RevisionStoreTest
extends MediaWikiTestCase
{
29 private function useTextId() {
30 global $wgMultiContentRevisionSchemaMigrationStage;
32 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD
);
36 * @param LoadBalancer $loadBalancer
37 * @param SqlBlobStore $blobStore
38 * @param WANObjectCache $WANObjectCache
40 * @return RevisionStore
42 private function getRevisionStore(
45 $WANObjectCache = null
47 global $wgMultiContentRevisionSchemaMigrationStage;
48 // the migration stage should be irrelevant, since all the tests that interact with
49 // the database are in RevisionStoreDbTest, not here.
51 return new RevisionStore(
52 $loadBalancer ?
: $this->getMockLoadBalancer(),
53 $blobStore ?
: $this->getMockSqlBlobStore(),
54 $WANObjectCache ?
: $this->getHashWANObjectCache(),
55 MediaWikiServices
::getInstance()->getCommentStore(),
56 MediaWikiServices
::getInstance()->getContentModelStore(),
57 MediaWikiServices
::getInstance()->getSlotRoleStore(),
58 MediaWikiServices
::getInstance()->getSlotRoleRegistry(),
59 $wgMultiContentRevisionSchemaMigrationStage,
60 MediaWikiServices
::getInstance()->getActorMigration()
65 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
67 private function getMockLoadBalancer() {
68 return $this->getMockBuilder( LoadBalancer
::class )
69 ->disableOriginalConstructor()->getMock();
73 * @return \PHPUnit_Framework_MockObject_MockObject|Database
75 private function getMockDatabase() {
76 return $this->getMockBuilder( Database
::class )
77 ->disableOriginalConstructor()->getMock();
81 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
83 private function getMockSqlBlobStore() {
84 return $this->getMockBuilder( SqlBlobStore
::class )
85 ->disableOriginalConstructor()->getMock();
89 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
91 private function getMockCommentStore() {
92 return $this->getMockBuilder( CommentStore
::class )
93 ->disableOriginalConstructor()->getMock();
97 * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
99 private function getMockSlotRoleRegistry() {
100 return $this->getMockBuilder( SlotRoleRegistry
::class )
101 ->disableOriginalConstructor()->getMock();
104 private function getHashWANObjectCache() {
105 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
108 public function provideSetContentHandlerUseDB() {
110 // ContentHandlerUseDB can be true of false pre migration.
111 [ false, SCHEMA_COMPAT_OLD
, false ],
112 [ true, SCHEMA_COMPAT_OLD
, false ],
113 // During and after migration it can not be false...
114 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
115 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
116 [ false, SCHEMA_COMPAT_NEW
, true ],
117 // ...but it can be true.
118 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
119 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
120 [ true, SCHEMA_COMPAT_NEW
, false ],
125 * @dataProvider provideSetContentHandlerUseDB
126 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
127 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
129 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
130 if ( $expectedFail ) {
131 $this->setExpectedException( MWException
::class );
134 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
136 $store = new RevisionStore(
137 $this->getMockLoadBalancer(),
138 $this->getMockSqlBlobStore(),
139 $this->getHashWANObjectCache(),
140 $this->getMockCommentStore(),
141 $nameTables->getContentModels(),
142 $nameTables->getSlotRoles(),
143 $this->getMockSlotRoleRegistry(),
145 MediaWikiServices
::getInstance()->getActorMigration()
148 $store->setContentHandlerUseDB( $contentHandlerDb );
149 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
153 * @covers \MediaWiki\Revision\RevisionStore::getTitle
155 public function testGetTitle_successFromPageId() {
156 $mockLoadBalancer = $this->getMockLoadBalancer();
157 // Title calls wfGetDB() so we have to set the main service
158 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
160 $db = $this->getMockDatabase();
161 // Title calls wfGetDB() which uses a regular Connection
162 $mockLoadBalancer->expects( $this->atLeastOnce() )
163 ->method( 'getConnection' )
166 // First call to Title::newFromID, faking no result (db lag?)
167 $db->expects( $this->at( 0 ) )
168 ->method( 'selectRow' )
174 ->willReturn( (object)[
175 'page_namespace' => '1',
176 'page_title' => 'Food',
179 $store = $this->getRevisionStore( $mockLoadBalancer );
180 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
182 $this->assertSame( 1, $title->getNamespace() );
183 $this->assertSame( 'Food', $title->getDBkey() );
187 * @covers \MediaWiki\Revision\RevisionStore::getTitle
189 public function testGetTitle_successFromPageIdOnFallback() {
190 $mockLoadBalancer = $this->getMockLoadBalancer();
191 // Title calls wfGetDB() so we have to set the main service
192 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
194 $db = $this->getMockDatabase();
195 // Title calls wfGetDB() which uses a regular Connection
196 // Assert that the first call uses a REPLICA and the second falls back to master
197 $mockLoadBalancer->expects( $this->exactly( 2 ) )
198 ->method( 'getConnection' )
200 // RevisionStore getTitle uses a ConnectionRef
201 $mockLoadBalancer->expects( $this->atLeastOnce() )
202 ->method( 'getConnectionRef' )
205 // First call to Title::newFromID, faking no result (db lag?)
206 $db->expects( $this->at( 0 ) )
207 ->method( 'selectRow' )
213 ->willReturn( false );
215 // First select using rev_id, faking no result (db lag?)
216 $db->expects( $this->at( 1 ) )
217 ->method( 'selectRow' )
219 [ 'revision', 'page' ],
223 ->willReturn( false );
225 // Second call to Title::newFromID, no result
226 $db->expects( $this->at( 2 ) )
227 ->method( 'selectRow' )
233 ->willReturn( (object)[
234 'page_namespace' => '2',
235 'page_title' => 'Foodey',
238 $store = $this->getRevisionStore( $mockLoadBalancer );
239 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
241 $this->assertSame( 2, $title->getNamespace() );
242 $this->assertSame( 'Foodey', $title->getDBkey() );
246 * @covers \MediaWiki\Revision\RevisionStore::getTitle
248 public function testGetTitle_successFromRevId() {
249 $mockLoadBalancer = $this->getMockLoadBalancer();
250 // Title calls wfGetDB() so we have to set the main service
251 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
253 $db = $this->getMockDatabase();
254 // Title calls wfGetDB() which uses a regular Connection
255 $mockLoadBalancer->expects( $this->atLeastOnce() )
256 ->method( 'getConnection' )
258 // RevisionStore getTitle uses a ConnectionRef
259 $mockLoadBalancer->expects( $this->atLeastOnce() )
260 ->method( 'getConnectionRef' )
263 // First call to Title::newFromID, faking no result (db lag?)
264 $db->expects( $this->at( 0 ) )
265 ->method( 'selectRow' )
271 ->willReturn( false );
273 // First select using rev_id, faking no result (db lag?)
274 $db->expects( $this->at( 1 ) )
275 ->method( 'selectRow' )
277 [ 'revision', 'page' ],
281 ->willReturn( (object)[
282 'page_namespace' => '1',
283 'page_title' => 'Food2',
286 $store = $this->getRevisionStore( $mockLoadBalancer );
287 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
289 $this->assertSame( 1, $title->getNamespace() );
290 $this->assertSame( 'Food2', $title->getDBkey() );
294 * @covers \MediaWiki\Revision\RevisionStore::getTitle
296 public function testGetTitle_successFromRevIdOnFallback() {
297 $mockLoadBalancer = $this->getMockLoadBalancer();
298 // Title calls wfGetDB() so we have to set the main service
299 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
301 $db = $this->getMockDatabase();
302 // Title calls wfGetDB() which uses a regular Connection
303 // Assert that the first call uses a REPLICA and the second falls back to master
304 $mockLoadBalancer->expects( $this->exactly( 2 ) )
305 ->method( 'getConnection' )
307 // RevisionStore getTitle uses a ConnectionRef
308 $mockLoadBalancer->expects( $this->atLeastOnce() )
309 ->method( 'getConnectionRef' )
312 // First call to Title::newFromID, faking no result (db lag?)
313 $db->expects( $this->at( 0 ) )
314 ->method( 'selectRow' )
320 ->willReturn( false );
322 // First select using rev_id, faking no result (db lag?)
323 $db->expects( $this->at( 1 ) )
324 ->method( 'selectRow' )
326 [ 'revision', 'page' ],
330 ->willReturn( false );
332 // Second call to Title::newFromID, no result
333 $db->expects( $this->at( 2 ) )
334 ->method( 'selectRow' )
340 ->willReturn( false );
342 // Second select using rev_id, result
343 $db->expects( $this->at( 3 ) )
344 ->method( 'selectRow' )
346 [ 'revision', 'page' ],
350 ->willReturn( (object)[
351 'page_namespace' => '2',
352 'page_title' => 'Foodey',
355 $store = $this->getRevisionStore( $mockLoadBalancer );
356 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
358 $this->assertSame( 2, $title->getNamespace() );
359 $this->assertSame( 'Foodey', $title->getDBkey() );
363 * @covers \MediaWiki\Revision\RevisionStore::getTitle
365 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
366 $mockLoadBalancer = $this->getMockLoadBalancer();
367 // Title calls wfGetDB() so we have to set the main service
368 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
370 $db = $this->getMockDatabase();
371 // Title calls wfGetDB() which uses a regular Connection
372 // Assert that the first call uses a REPLICA and the second falls back to master
374 // RevisionStore getTitle uses getConnectionRef
375 // Title::newFromID uses getConnection
376 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
377 $mockLoadBalancer->expects( $this->exactly( 2 ) )
379 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
380 static $callCounter = 0;
382 // The first call should be to a REPLICA, and the second a MASTER.
383 if ( $callCounter === 1 ) {
384 $this->assertSame( DB_REPLICA
, $masterOrReplica );
385 } elseif ( $callCounter === 2 ) {
386 $this->assertSame( DB_MASTER
, $masterOrReplica );
391 // First and third call to Title::newFromID, faking no result
392 foreach ( [ 0, 2 ] as $counter ) {
393 $db->expects( $this->at( $counter ) )
394 ->method( 'selectRow' )
400 ->willReturn( false );
403 foreach ( [ 1, 3 ] as $counter ) {
404 $db->expects( $this->at( $counter ) )
405 ->method( 'selectRow' )
407 [ 'revision', 'page' ],
411 ->willReturn( false );
414 $store = $this->getRevisionStore( $mockLoadBalancer );
416 $this->setExpectedException( RevisionAccessException
::class );
417 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
420 public function provideNewRevisionFromRow_legacyEncoding_applied() {
421 yield
'windows-1252, old_flags is empty' => [
426 'old_text' => "S\xF6me Content",
431 yield
'windows-1252, old_flags is null' => [
436 'old_text' => "S\xF6me Content",
443 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
445 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
447 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
448 if ( !$this->useTextId() ) {
449 $this->markTestSkipped( 'No longer applicable with MCR schema' );
452 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
453 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
455 $blobStore = new SqlBlobStore( $lb, $cache );
456 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
458 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
460 $record = $store->newRevisionFromRow(
461 $this->makeRow( $row ),
463 Title
::newFromText( __METHOD__
. '-UTPage' )
466 $this->assertSame( $text, $record->getContent( SlotRecord
::MAIN
)->serialize() );
470 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
472 public function testNewRevisionFromRow_legacyEncoding_ignored() {
473 if ( !$this->useTextId() ) {
474 $this->markTestSkipped( 'No longer applicable with MCR schema' );
478 'old_flags' => 'utf-8',
479 'old_text' => 'Söme Content',
482 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
483 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
485 $blobStore = new SqlBlobStore( $lb, $cache );
486 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
488 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
490 $record = $store->newRevisionFromRow(
491 $this->makeRow( $row ),
493 Title
::newFromText( __METHOD__
. '-UTPage' )
495 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord
::MAIN
)->serialize() );
498 private function makeRow( array $array ) {
502 'rev_timestamp' => '20110101000000',
503 'rev_user_text' => 'Tester',
505 'rev_minor_edit' => 0,
508 'rev_parent_id' => 0,
509 'rev_sha1' => 'deadbeef',
510 'rev_comment_text' => 'Testing',
511 'rev_comment_data' => '{}',
512 'rev_comment_cid' => 111,
513 'page_namespace' => 0,
514 'page_title' => 'TEST',
517 'page_is_redirect' => 0,
519 'user_name' => 'Tester',
522 if ( $this->useTextId() ) {
524 'rev_content_format' => CONTENT_FORMAT_TEXT
,
525 'rev_content_model' => CONTENT_MODEL_TEXT
,
528 'old_text' => 'Hello World',
529 'old_flags' => 'utf-8',
532 if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
534 'main' => new WikitextContent( $array['old_text'] ),
542 public function provideMigrationConstruction() {
544 [ SCHEMA_COMPAT_OLD
, false ],
545 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
546 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
547 [ SCHEMA_COMPAT_NEW
, false ],
548 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
549 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
550 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
555 * @covers \MediaWiki\Revision\RevisionStore::__construct
556 * @dataProvider provideMigrationConstruction
558 public function testMigrationConstruction( $migration, $expectException ) {
559 if ( $expectException ) {
560 $this->setExpectedException( InvalidArgumentException
::class );
562 $loadBalancer = $this->getMockLoadBalancer();
563 $blobStore = $this->getMockSqlBlobStore();
564 $cache = $this->getHashWANObjectCache();
565 $commentStore = $this->getMockCommentStore();
566 $services = MediaWikiServices
::getInstance();
567 $nameTables = $services->getNameTableStoreFactory();
568 $contentModelStore = $nameTables->getContentModels();
569 $slotRoleStore = $nameTables->getSlotRoles();
570 $slotRoleRegistry = $services->getSlotRoleRegistry();
571 $store = new RevisionStore(
576 $nameTables->getContentModels(),
577 $nameTables->getSlotRoles(),
580 $services->getActorMigration()
582 if ( !$expectException ) {
583 $store = TestingAccessWrapper
::newFromObject( $store );
584 $this->assertSame( $loadBalancer, $store->loadBalancer
);
585 $this->assertSame( $blobStore, $store->blobStore
);
586 $this->assertSame( $cache, $store->cache
);
587 $this->assertSame( $commentStore, $store->commentStore
);
588 $this->assertSame( $contentModelStore, $store->contentModelStore
);
589 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
590 $this->assertSame( $migration, $store->mcrMigrationStage
);